/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 4; tab-width: 4 -*- */
/*
 *  Copyright 2008 Global Sign In
 * 
 *  This code is free software; you can redistribute it and/or
 *  modify it under the terms of the GNU Lesser General Public
 *  License as published by the Free Software Foundation; either
 *  version 2 of the License, or (at your option) any later version.
 *
 *  This library is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 *  Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public
 *  License along with this library; if not, write to the Free Software
 *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 */

#include <signal.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <string.h>
#include <strings.h>
#include <pthread.h>
#include <fcgio.h>
#include <libintl.h>
#include <map>
#include <iostream>
#include <fstream>
#include <algorithm>

#include "config.h"
#include "AuthSQL.h"
#include "Base64.h"
#include "CampaignSQL.h"
#include "ConfigurationFile.h"
#include "DBUsageLogger.h"
#include "HMAC.h"
#include "MySQLBase.h"
#include "TimeConverter.h"
#include "URLEncoding.h"
#include "WebAPI.h"

using namespace std;

static bool g_mustQuit = false;
static ofstream g_outputFile;
static string g_outputFileName;
static streambuf *g_clogBuff = NULL;

/// Catch signals and take the appropriate action.
static void catchSignals(int sigNum)
{
	if (sigNum != SIGCHLD)
	{
		g_mustQuit = true;
	}
}

/// Close everything at exit.
static void closeAll(void)
{
	if (g_clogBuff != NULL)
	{
		// Restore log output
		clog.rdbuf(g_clogBuff);
		g_outputFile.close();
		unlink(g_outputFileName.c_str());
	}
}

static char *getContent(unsigned int contentLength)
{
	char *pBuffer = NULL;

	if (contentLength > 0)
	{
		// Read the whole input
		contentLength = min(contentLength, (unsigned int)1000000);
		if (contentLength > 0)
		{
			pBuffer = new char[contentLength + 1];
			cin.read(pBuffer, contentLength);
			contentLength = cin.gcount();
			pBuffer[contentLength] = '\0';
		}
	}

	// Chew up any remaining stdin - this shouldn't be necessary
	// but is because mod_fastcgi doesn't handle it correctly.
	// ignore() doesn't set the eof bit in some versions of glibc++
	// so use gcount() instead of eof()...
	do
	{
		cin.ignore(1024);
	} while (cin.gcount() == 1024);

	return pBuffer;
}

static string getHeaderValue(const char *pContent, const char *pHeadersEnd,
	const string &headerName)
{
	if ((pContent == NULL) ||
		(headerName.empty() == true))
	{
		return "";
	}

	char *pHeaderStart = strstr(pContent, headerName.c_str());
	if ((pHeaderStart != NULL) &&
		(pHeaderStart < pHeadersEnd))
	{
		char *pHeaderEnd = strstr(pHeaderStart, "\r\n");

		if (pHeaderEnd == NULL)
		{
			pHeaderEnd = strstr(pHeaderStart, "\n");
		}

		if ((pHeaderEnd != NULL) &&
			(pHeaderEnd <= pHeadersEnd))
		{
			return string(pHeaderStart + headerName.length(),
				pHeaderEnd - pHeaderStart - headerName.length());
		}
	}

	return "";
}

static void parseAuth(const string &authHeaderValue, string &authScheme, string &appKeyId, string &signature)
{
	string::size_type spacePos = authHeaderValue.find(' ');

	if (spacePos == string::npos)
	{
		return;
	}

	// Expected format is "authScheme appKeyId:signature"
	authScheme = authHeaderValue.substr(0, spacePos);
	string::size_type colonPos = authHeaderValue.find(':', spacePos);
	if (colonPos == string::npos)
	{
		return;
	}

	appKeyId = authHeaderValue.substr(spacePos + 1, colonPos - spacePos - 1);
	signature = authHeaderValue.substr(colonPos + 1);
}

static bool verifyAuthentication(MySQLBase *pDb, DBUsageLogger &usageLogger,
	const string &dateHeaderValue, const string &authHeaderValue,
	time_t timeNow, char *pXml, unsigned int xmlLength)
{
	AuthSQL authData(pDb);
	time_t requestTime = TimeConverter::fromTimestamp(dateHeaderValue);
	bool goodAuth = false;

	if (dateHeaderValue.empty() == true)
	{
		cout << "<Error>No date</Error>\r\n";
		usageLogger.logRequest("", 3);
		return false;
	}
	else if (authHeaderValue.empty() == true)
	{
		cout << "<Error>No authentication</Error>\r\n";
		usageLogger.logRequest("", 4);
		return false;
	}

#ifdef DEBUG
	cout << "<RequestDate>" << dateHeaderValue << "</RequestDate>\r\n";
	cout << "<RequestTime>" << requestTime << "</RequestTime>\r\n";
	cout << "<CurrentTime>" << timeNow << "</CurrentTime>\r\n";
	string xmlOnly(pXml, xmlLength);
	cout << "<XmlInput><![CDATA[" << xmlOnly << "]]></XmlInput>\r\n";
	cout << "<XmlInputLength>" << xmlLength << "</XmlInputLength>\r\n";
#endif

	// Protect against replay attacks and check the request date is +/- 15 minutes the local time
	double timeDiff = difftime(max(requestTime, timeNow), min(requestTime, timeNow));
	if (timeDiff > 15 * 60)
	{
		cout << "<Error>Request date too skewed</Error>\r\n";
		usageLogger.logRequest("", 5);
	}
	else
	{
		string authScheme, appKeyId, signature;

		parseAuth(authHeaderValue, authScheme, appKeyId, signature);
#ifdef DEBUG
		cout << "<AuthScheme>" << authScheme << "</AuthScheme>\r\n";
		cout << "<KeyId>" << appKeyId << "</KeyId>\r\n";
		cout << "<UserSignature>" << signature << "</UserSignature>\r\n";
#endif

		// Is this GiveMailAuthentication ? ;-)
		if (authScheme != "GMA")
		{
			cout << "<Error>Unsupported authentication scheme</Error>\r\n";
			usageLogger.logRequest("", 6);
		}
		else if (appKeyId.empty() == true)
		{
			cout << "<Error>No key ID</Error>\r\n";
			usageLogger.logRequest("", 7);
		}
		else if (signature.empty() == true)
		{
			cout << "<Error>No signature</Error>\r\n";
			usageLogger.logRequest("", 8);
		}
		else
		{
			Key *pKey = authData.getKey(appKeyId);
			if (pKey == NULL)
			{
				cout << "<Error>No such key ID</Error>\r\n";
				usageLogger.logRequest("", 9);
			}
			else
			{
				// Tell the UsageLogger what key this is
				usageLogger.setKeyId(pKey->m_dbId);
				// ...and what hash identifies the request
				usageLogger.setHash(signature);

				// Has the key expired ?
				double keyDiff = pKey->getTimeDiff(timeNow);
				if (keyDiff < 0)
				{
					cout << "<Error>Key has expired</Error>\r\n";
					usageLogger.logRequest("", 10);

					delete pKey;
					pKey = NULL;
				}
				else
				{
					cout << "<KeyExpiry>" << (time_t)keyDiff << "</KeyExpiry>\r\n";

					// Was the same request issued recently ?
					time_t lastRequestTime = usageLogger.findLastRequestTime();
					if (lastRequestTime > 0)
					{
#ifdef DEBUG
						cout << "<LastRequestTime>" << lastRequestTime << "</LastRequestTime>\r\n";
#endif

						timeDiff = difftime(timeNow, lastRequestTime);
						if (timeDiff < 1 * 60)
						{
							cout << "<Error>Request reissued too soon</Error>\r\n";
							// Better not to log this to the database to prevent DoS attacks
							// usageLogger.logRequest("", 11);

							delete pKey;
							pKey = NULL;
						}
					}
				}
			}

			if (pKey != NULL)
			{
				string payload(dateHeaderValue);

				payload += "\n"; 
				payload += pXml;

				unsigned long dataLength = (unsigned long)payload.length();

				// Sign the XML
				char *pEncodedSignature = HMAC::sign(pKey, payload.c_str(), dataLength);
				if ((pEncodedSignature != NULL) &&
					(dataLength > 0))
				{
					string actualSignature(pEncodedSignature, dataLength);

#ifdef DEBUG
					cout << "<ActualSignature>" << actualSignature << "</ActualSignature>\r\n";
#endif

					// Verify the signature
					if (actualSignature != signature)
					{
						cout << "<Error>HMAC verification failed</Error>\r\n";
						usageLogger.logRequest("", 12);
					}
					else
					{
						goodAuth = true;
					}

					delete[] pEncodedSignature;
				}

				delete pKey;
			}
		}
	}

	return goodAuth;
}

static bool processXml(MySQLBase *pDb, unsigned int contentLength,
	bool openedConfiguration, bool openedDatabase,
	const string &remoteAddress = string(""), unsigned int remotePort = 0,
	const string &contentType = string(""))
{
	bool checkAuth = true;

	// Local requests don't have to be authenticated
	if (remoteAddress == "127.0.0.1")
	{
		checkAuth = false;
	}

	cout << "Content-Type: text/xml\r\n\r\n";
	cout << "<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n";
	cout << "<GiveMail>\r\n";

	if (openedConfiguration == false)
	{
		cout << "<Error>Couldn't open configuration file</Error>\r\n";
		cout << "</GiveMail>\r\n";
		return false;
	}
	else if (openedDatabase == false)
	{
		cout << "<Error>Couldn't open database</Error>\r\n";
		cout << "</GiveMail>\r\n";
		return false;
	}

	char *pContent = getContent(contentLength);
	if (pContent == NULL)
	{
		cout << "<Error>Empty request</Error>\r\n";
		cout << "</GiveMail>\r\n";
		return false;
	}
#ifdef DEBUG
	cout << "<Content><![CDATA[" << pContent << "]]></Content>\r\n";
	cout << "<ContentLength>" << contentLength << "</ContentLength>\r\n";
	cout << "<LogFile IsOpen=" << g_outputFile.is_open() << ">" << g_outputFileName << "</LogFile>\r\n";
#endif

	string unescapedXml;
	bool deleteContent = true;

	if (strncasecmp(contentType.c_str(),
		"application/x-www-form-urlencoded", contentType.length()) == 0)
	{
		unescapedXml = URLEncoding::decode(pContent, contentLength);

		delete[] pContent;
		deleteContent = false;

		pContent = const_cast<char*>(unescapedXml.c_str());
		contentLength = unescapedXml.length();
	}

	CampaignSQL campaignData(pDb);
	DBUsageLogger usageLogger(pDb, remoteAddress, remotePort);
	WebAPI api(&campaignData, &usageLogger, cout);
	string dateHeaderValue, authHeaderValue;
	char *pXml = NULL;
	unsigned int xmlLength = 0;
	time_t timeNow = time(NULL);
	bool parsedOk = false;

	// Skip headers and ignore trailing data, eg boundary
	char *pGiveMailBlockStart = NULL;
	if (pContent != NULL)
	{
		pGiveMailBlockStart = strstr(pContent, "<GiveMail>");
	}
	if (pGiveMailBlockStart == NULL)
	{
		cout << "<Error>No GiveMail block</Error>\r\n";
		usageLogger.logRequest("", 1);
	}
	else
	{
		char *pHeadersEnd = strstr(pContent, "\r\n\r\n");
		unsigned int headersEndLength = 4;

		// Any headers ?
		if (pHeadersEnd == NULL)
		{
			pHeadersEnd = strstr(pContent, "\n\n");
			headersEndLength = 2;
		}

		if ((pHeadersEnd != NULL) &&
			(pHeadersEnd < pGiveMailBlockStart))
		{
			unsigned int shavedAtStart = (pHeadersEnd - pContent) + headersEndLength;

			// Look for Date and Authorization in headers
			dateHeaderValue = getHeaderValue(pContent, pHeadersEnd, "Date: ");
			// Authentication would be a better choice, but HTTP doesn't define it
			authHeaderValue = getHeaderValue(pContent, pHeadersEnd, "Authorization: ");

			char *pGiveMailBlockEnd = strstr(pContent, "</GiveMail>");
			if (pGiveMailBlockEnd != NULL)
			{
				unsigned int shavedAtEnd = (pContent + contentLength) - (pGiveMailBlockEnd + 11);

				pXml = pHeadersEnd + headersEndLength;
				xmlLength = (unsigned int)(contentLength - shavedAtStart - shavedAtEnd);
			}
			else
			{
				pXml = NULL;
				xmlLength = 0;
			}
		}
		else
		{
			pXml = pContent;
			xmlLength = contentLength;
		}
	}

	if ((pXml == NULL) ||
		(xmlLength == 0))
	{
		cout << "<Error>Empty or malformed request</Error>\r\n";
		usageLogger.logRequest("", 2);
	}
	else if ((checkAuth == false) ||
		(verifyAuthentication(pDb, usageLogger, dateHeaderValue, authHeaderValue,
			timeNow, pXml, xmlLength) == true))
	{
		parsedOk = api.parse(pXml, xmlLength);
	}
	cout << "</GiveMail>\r\n";

	if (deleteContent == true)
	{
		delete[] pContent;
	}

	return parsedOk;
}

int main(int argc, char **argv)
{
	struct sigaction quitAction, ignAction;
	string configFileName("/etc/givemail/conf.d/givemail.conf");
	streambuf *cinBuff  = cin.rdbuf();
	g_clogBuff = clog.rdbuf();
	streambuf *coutBuff = cout.rdbuf();
	streambuf *cerrBuff = cerr.rdbuf();
	bool openedConfiguration = false, openedDatabase = false;

#if defined(ENABLE_NLS)
	bindtextdomain(GETTEXT_PACKAGE, PACKAGE_LOCALE_DIR);
	bind_textdomain_codeset(GETTEXT_PACKAGE, "UTF-8");
	textdomain(GETTEXT_PACKAGE);
#endif

	// Catch SIGINT, QUIT and CHLD
	sigemptyset(&quitAction.sa_mask);
	quitAction.sa_flags = 0;
	quitAction.sa_handler = catchSignals;
	sigaction(SIGINT, &quitAction, NULL);
	sigaction(SIGQUIT, &quitAction, NULL);
	sigaction(SIGCHLD, &quitAction, NULL);
	sigemptyset(&ignAction.sa_mask);
	// Ignore SIGPIPE
	ignAction.sa_flags = 0;
	ignAction.sa_handler = SIG_IGN;
	sigaction(SIGPIPE, &ignAction, NULL);

	atexit(closeAll);

	// Redirect log output to a temporary file
	char outTemplate[18] = "/tmp/webapiXXXXXX";
        int outFd = mkstemp(outTemplate);
        if (outFd != -1)
        {
		close(outFd);
		g_outputFile.open(outTemplate);
		g_outputFileName = outTemplate;
		clog.rdbuf(g_outputFile.rdbuf());
        }

	// This environment variable should be declared in /etc/httpd/conf.d/fcgid.conf with
	// DefaultInitEnv WEBAPI_CONFIGURATION_FILE /etc/givemail/conf.d/myconfig.conf
	char *pConfigFile = getenv("WEBAPI_CONFIGURATION_FILE");
	if (pConfigFile != NULL)
	{
		configFileName = pConfigFile;
	}

	// Load configuration
	ConfigurationFile *pConfig = ConfigurationFile::getInstance(configFileName);
	if ((pConfig == NULL) ||
		(pConfig->parse() == false))
	{
		clog << "Couldn't open configuration file " << configFileName << endl;
	}
	else
	{
		openedConfiguration = true;
	}

	// Open the database
	MySQLBase db(pConfig->m_hostName, pConfig->m_databaseName,
		pConfig->m_userName, pConfig->m_password);
	openedDatabase = db.isOpen();

#ifdef DEBUG
	char *pNoFcgi = getenv("WEBAPI_NO_FCGI");
	if ((pNoFcgi != NULL) &&
		(strncasecmp(pNoFcgi, "Y", 1) == 0))
	{
		cin.seekg(0, ios::end);
		unsigned int contentLength = cin.tellg();
		cin.seekg(0, ios::beg);

		processXml(&db, contentLength,
			openedConfiguration, openedDatabase,
			"127.0.0.1");

		return EXIT_SUCCESS;
	}
#endif

	FCGX_Request request;
	FCGX_Init();
	FCGX_InitRequest(&request, 0, 0);

	// Loop until we have to exit
	while ((g_mustQuit == false) &&
		(FCGX_Accept_r(&request) == 0))
	{
		fcgi_streambuf cin_fcgi_streambuf(request.in);
		fcgi_streambuf cout_fcgi_streambuf(request.out);
		fcgi_streambuf cerr_fcgi_streambuf(request.err);
		string remoteAddress, contentType;
		unsigned int contentLength = 0, remotePort = 0;

		// Redirect standard input and outputs
		cin.rdbuf(&cin_fcgi_streambuf);
		cout.rdbuf(&cout_fcgi_streambuf);
		cerr.rdbuf(&cerr_fcgi_streambuf);

		char *pContentLength = FCGX_GetParam("CONTENT_LENGTH", request.envp);
		if (pContentLength != NULL)
		{
			contentLength = (unsigned int)atoi(pContentLength);
		}

		unsigned int envNum = 0;
		for (char *pEnv = request.envp[envNum]; pEnv != NULL; pEnv = request.envp[++envNum])
		{
			string envVar(pEnv);

			string::size_type pos = envVar.find("REMOTE_ADDR=");
			if (pos != string::npos)
			{
				remoteAddress = envVar.substr(pos + 12);
			}
			pos = envVar.find("REMOTE_PORT=");
			if (pos != string::npos)
			{
				remotePort = (unsigned int)atoi(envVar.substr(pos + 12).c_str());
			}
			pos = envVar.find("CONTENT_TYPE=");
			if (pos != string::npos)
			{
				contentType = envVar.substr(pos + 13);
			}
		}

		try
		{
			processXml(&db, contentLength,
				openedConfiguration, openedDatabase,
				remoteAddress, remotePort, contentType);
		}
		catch (const exception &e)
		{
			clog << "Caught exception ! " << e.what() << endl;
		}
		catch (...)
		{
			clog << "Caught unknown exception !" << endl;
		}

#if 0
		envNum = 0;
		for (char *pEnv = request.envp[envNum]; pEnv != NULL; pEnv = request.envp[++envNum])
		{
			cout << "<Env><![CDATA[" << pEnv << "]]></Env>\r\n";
		}
#endif

		// Restore standard input and outputs
		cin.rdbuf(cinBuff);
		cout.rdbuf(coutBuff);
		cerr.rdbuf(cerrBuff);
	}

	// FIXME: delete the ConfigurationFile instance

	return EXIT_SUCCESS;
}

